package itsm.isperp.framework.data.mybatis.plugin;

import itsm.isperp.framework.collection.tree.ITreeNode;
import itsm.isperp.framework.data.dialect.DialectFactory;
import itsm.isperp.framework.data.domain.DataResponse;
import itsm.isperp.framework.data.mybatis.mapping.BoundSqlSqlSource;
import itsm.isperp.framework.data.mybatis.utils.MappedStatementUtil;
import itsm.isperp.framework.web.domain.CountQuery;
import itsm.isperp.framework.web.request.JqGridFilter;
import itsm.isperp.framework.web.request.JqGridFilterRule;
import itsm.isperp.framework.web.request.JqGridRequest;
import itsm.isperp.module.utils.ProjectDomainUtils;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.URLDecoder;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.scripting.xmltags.ForEachSqlNode;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;

/**
 * 通过切入方式拦截Filter接口为参数的查询方法，集成Mybatis的拦截器插件
 * 
 * @author lizx
 * @date 2013-1-24
 * @version V1.0
 */
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {
		MappedStatement.class, Object.class, RowBounds.class,
		ResultHandler.class }) })
public class JqGridMyBatisPlugin implements Interceptor {

	static java.util.concurrent.ConcurrentHashMap<String, Object[]> cache = new ConcurrentHashMap<>();

	@Override
	public Object intercept(Invocation arg0) throws Throwable {

		boolean pageable = false;
		JqGridRequest req = null;
		MappedStatement mappedStatement = null;
		for (Object o : arg0.getArgs()) {
			if (o != null) {
				if (JqGridRequest.class.isAssignableFrom(o.getClass())) {
					pageable = true;
					req = (JqGridRequest) o;
				} else if (o instanceof MappedStatement) {
					mappedStatement = (MappedStatement) o;
				} else if (o instanceof MapperMethod.ParamMap<?>) {
					MapperMethod.ParamMap<?> mp = (MapperMethod.ParamMap<?>) o;

					for (Iterator<String> itr = mp.keySet().iterator(); itr
							.hasNext();) {
						String key = itr.next();
						if (mp.get(key) instanceof JqGridRequest) {
							pageable = true;
							req = (JqGridRequest) mp.get(key);
							break;
						}
					}
				}
			}
		}

		if (pageable && req != null && mappedStatement != null) {
			return findAll(arg0, mappedStatement, req);
		}
		return arg0.proceed();
	}

	private static final DataResponse<Object> findAll(Invocation arg0,
			MappedStatement mappedStatement, JqGridRequest req)
			throws SQLException, InvocationTargetException,
			IllegalAccessException {

		int total = 0;
		final Object[] args = arg0.getArgs();
		Executor exec = (Executor) arg0.getTarget();
		Statement sqlstmt = exec.getTransaction().getConnection()
				.createStatement();

		BoundSql boundSql = mappedStatement.getBoundSql(args[1]);
		Pattern p = Pattern.compile("\n|\t");
		Matcher m = p.matcher(boundSql.getSql());

		final String navSql = m.replaceAll(" ");
		// 判断是否包含子查询 FROM( )T WHERE 1=1

		// 根据请求器构造查询语句
		JqGridFilter filter = req.getFilter();

		StringBuilder pageSql = new StringBuilder();

		// 判断子查询
		String pageSqlStr = "";
		String mappedId = mappedStatement.getId();
		boolean nomralSelect = true;
		String countQuerySql = null;
		if (mappedId.contains("DetailByRequest")
				&& !navSql.contains("UNION ALL") && !navSql.contains("WHEN")) {

			String[] cArr = null;
			String formT = null;
			String columns = null;

			// 此处需要缓存避免重复解析mapper中的sql
			nomralSelect = false;
			if (!cache.containsKey(mappedId)) {

				String[] moduleNameArr = mappedId.split("\\.");
				String moduleName = moduleNameArr[moduleNameArr.length - 2];
				String tableName = itsm.isperp.framework.utils.StringUtils
						.underscore(moduleName.substring(0,
								moduleName.length() - 6));
				String sql = navSql;
				int i = navSql.indexOf("FROM (SELECT");
				if (i >= 0) {
					sql = navSql.substring(i + 12);
					i = sql.lastIndexOf(")");
					sql = sql.substring(0, i);
				} else {

					i = sql.indexOf("FROM (SELECT");

					if (i >= 0) {
						sql = sql.substring(i + 12);
						i = sql.lastIndexOf(")");
						sql = sql.substring(0, i);
					} else {
						nomralSelect = true;

					}

				}
				Object[] cacheObj = new Object[4];

				i = sql.indexOf(tableName);
				formT = "FROM " + sql.substring(i);

				columns = sql.substring(0, i);
				i = columns.lastIndexOf("FROM");
				if (i == -1) {
					i = columns.lastIndexOf("from");
					if (i == -1) {
						i = columns.lastIndexOf("From");
					}
				}

				columns = columns.substring(0, i);

				if (StringUtils.isEmpty(columns)) {
					cArr = new String[] { "*" };
				} else {
					cArr = columns.split(",");
				}
				cacheObj[0] = formT;
				cacheObj[1] = columns;
				cacheObj[2] = cArr;
				cacheObj[3] = nomralSelect;

				cache.put(mappedId, cacheObj);
			} else {
				Object[] cacheObj = cache.get(mappedId);
				formT = (String) cacheObj[0];
				columns = (String) cacheObj[1];
				cArr = (String[]) cacheObj[2];
				nomralSelect = (boolean) cacheObj[3];

			}

			if (!nomralSelect) {

				String[] sArr = req.getSelectColumns();

				if (sArr != null) {

					for (int g = 0; g < sArr.length; g++) {
						if (StringUtils.isEmpty(sArr[g])) {
							continue;
						}

						String s = sArr[g].trim();
						boolean notFind = true;

						if (!s.startsWith("COUNT") && !s.startsWith("CASE")
								&& !s.startsWith("DISTINCT")) {

							FIND_COLUMN: for (int f = 0; f < cArr.length; f++) {
								String c = cArr[f].trim();

								int idx = c.lastIndexOf(s);
								if (c.endsWith(s)
										&& (idx > 0 && !"_".equals(c.substring(
												idx - 1, idx)))) {

									sArr[g] = c;
									notFind = false;
									break FIND_COLUMN;
								}
							}
							if (notFind && !s.contains(".")) {
								sArr[g] = "a." + s;
							}
						}

					}
				}

				pageSql.append(formT);

				if (StringUtils.isNotEmpty(req.getJoinString())) {
					pageSql.append(req.getJoinString());
				}

				// 查询字段问题
				if (filter != null) {

					List<JqGridFilterRule> rules = filter.getRules();

					for (JqGridFilterRule r : rules) {

						String s = r.getField().trim();

						boolean notFind = true;
						FIND_COLUMN: for (int f = 0; f < cArr.length; f++) {

							String c = cArr[f].trim();
							int idx = c.lastIndexOf(s);
							if (c.endsWith(s)
									&& !"_".equals(c.substring(idx - 1, idx))) {
								int asIndex = c.indexOf(" AS ");
								if (asIndex > 0) {
									c = c.substring(0, asIndex);
								} else {
									asIndex = c.indexOf(" as ");
									if (asIndex > 0) {
										c = c.substring(0, asIndex);
									}
								}

								r.setField(c);
								notFind = false;
								break FIND_COLUMN;
							}
						}
						if (notFind && !s.startsWith("COUNT")
								&& !s.startsWith("CASE")
								&& !s.startsWith("DISTINCT")
								&& !s.contains(".")) {
							r.setField("a." + s);
						}

					}

					req.setSelectColumns(sArr);
					if (pageSql.indexOf(" WHERE ") < 0)
						pageSql.append(" WHERE 1=1 ");
					else if(pageSql.indexOf(" WHERE ") >0 && (pageSql.lastIndexOf(")")>pageSql.lastIndexOf(" WHERE ")))
						pageSql.append(" WHERE 1=1 ");
					pageSql.append(filter.getSql());
				} else {
					if (pageSql.indexOf(" WHERE ") < 0)
						pageSql.append(" WHERE 1=1 ");
				}

				if (StringUtils.isNotEmpty(req.getSearchString())) {
					pageSql.append(" AND (")
							.append(req.getSearchString()
									.replaceAll("process_id", "a.process_id")
									.replaceFirst("project_id", "a.project_id")
									.replaceAll("creator", "a.creator")
									.replaceAll(",status", ",a.status")
									.replaceAll(" status", "a.status"))
							.append(")");
				}
				if (req.getProjectDomain() != null) {
					pageSql.append(ProjectDomainUtils
							.getUserDomainSql(req.getProjectDomain())
							.replaceAll("process_id", "a.process_id")
							.replaceFirst("project_id", "a.project_id")
							.replaceAll("creator", "a.creator")
							.replaceAll(",status", ",a.status")
							.replaceAll(" status", "a.status"));

				}

				if (StringUtils.isNotEmpty(req.getGroupBy())) {
					pageSql.append(" GROUP BY ").append(req.getGroupBy());
				}

				countQuerySql = pageSql.toString();
				if (sArr == null) {
					pageSqlStr = "SELECT " + columns + " " + pageSql.toString();
				} else {

					pageSqlStr = "SELECT " + req.getSelectColumnsString() + " "
							+ pageSql.toString();
				}
			}

		}

		if (nomralSelect) {

			countQuerySql = navSql;
			if (StringUtils.isNotEmpty(req.getSearchField())) {
				pageSql.append("SELECT ").append(req.getSelectColumnsString())
						.append("  FROM (").append("SELECT * ")
						.append(req.getSearchField()).append(" ")
						.append(navSql).append(" ");

				if (StringUtils.isNotEmpty(req.getGroupBy())) {
					pageSql.append(" GROUP BY ").append(req.getGroupBy());
				}
				pageSql.append(")  t WHERE 1=1");
			} else {
				pageSql.append("SELECT ").append(req.getSelectColumnsString())
						.append(navSql);
			}

			if (filter != null)
				pageSql.append(filter.getSql());

			if (req.getProjectDomain() != null) {
				pageSql.append(ProjectDomainUtils.getUserDomainSql(req
						.getProjectDomain()));
			}

			if (StringUtils.isNotEmpty(req.getSearchString())) {
				pageSql.append(" AND (").append(req.getSearchString())
						.append(")");
			}

			if (StringUtils.isNotEmpty(req.getGroupBy())
					&& StringUtils.isEmpty(req.getSearchField())) {
				pageSql.append(" GROUP BY ").append(req.getGroupBy());
			}
			pageSqlStr = pageSql.toString();
		}

		JqGridResponseData<Object> rep = new JqGridResponseData<Object>();
		if (req.getCountQuery() != null) {
			String queryPath = req.getQueryPath();
			CountQuery cq = req.getCountQuery();
			if (StringUtils.isEmpty(queryPath)) {
				queryPath = cq.getFisrtQueryKey();
			}

			String filtersql = null;
			if ("null".equals(queryPath) || queryPath == null) {
				filtersql = cq.getFilter();
			} else {
				try {
					queryPath = URLDecoder.decode(queryPath, "UTF-8");
				} catch (UnsupportedEncodingException e) {
				}
				filtersql = cq.getFilter(queryPath);

			}

			pageSqlStr += " AND " + filtersql;
			pageSql.append(" AND " + filtersql);
			if (!req.isNotMenu()) {

				cq.from(countQuerySql);
				cq.setFromRequest(true);
				ITreeNode[] menudata = cq.getTreeNodes();
				rep.setMenudata(menudata);
			}

		}

		// 判断是否分页查询
		if (!"excel".equals(req.getOper())
				&& (req.isOnlyQueryCount() || req.isPageable())) {

			Connection connection = sqlstmt.getConnection();
			Object parameterObject = boundSql.getParameterObject();
			String countSql = null;

			if (nomralSelect || StringUtils.isNotEmpty(req.getGroupBy())) {
				countSql = "select count(0) from (" + pageSqlStr + ")T ";
			} else {
				countSql = "select count(0)  " + pageSql + " ";
			}

			PreparedStatement countStmt = connection.prepareStatement(countSql);
			BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(),
					countSql, boundSql.getParameterMappings(), parameterObject);
			setParameters(countStmt, mappedStatement, countBS, parameterObject);
			ResultSet rs = countStmt.executeQuery();
			// int count = 0;
			if (rs.next()) {
				total = rs.getInt(1);
			}
			rs.close();
			countStmt.close();

		}

		pageSqlStr += req.getOrderSql();

		if (!"excel".equals(req.getOper())
				&& (req.isPageable() || req.getRows() >= 1)) {

			pageSqlStr = DialectFactory.getDialect().getLimitString(pageSqlStr,
					req.getOffset(), req.getRows());

		}

		args[2] = RowBounds.DEFAULT;
		BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(),
				pageSqlStr, boundSql.getParameterMappings(),
				boundSql.getParameterObject());

		MappedStatement newMs = MappedStatementUtil.copyFromMappedStatement(
				mappedStatement, new BoundSqlSqlSource(newBoundSql));

		args[0] = newMs;

		Object ret = null;
		if (!req.isOnlyQueryCount()) {
			ret = arg0.proceed();
		} else {
			ret = new ArrayList<Object>();
		}

		rep.setResult((List<Object>) ret, req, total);

		return rep;
	}

	@Override
	public Object plugin(Object arg0) {
		return Plugin.wrap(arg0, (Interceptor) this);
	}

	@Override
	public void setProperties(Properties arg0) {
	}

	@SuppressWarnings("unchecked")
	private static void setParameters(PreparedStatement ps,
			MappedStatement mappedStatement, BoundSql boundSql,
			Object parameterObject) throws SQLException {
		ErrorContext.instance().activity("setting parameters")
				.object(mappedStatement.getParameterMap().getId());
		List<ParameterMapping> parameterMappings = boundSql
				.getParameterMappings();
		if (parameterMappings != null) {
			Configuration configuration = mappedStatement.getConfiguration();
			TypeHandlerRegistry typeHandlerRegistry = configuration
					.getTypeHandlerRegistry();
			MetaObject metaObject = parameterObject == null ? null
					: configuration.newMetaObject(parameterObject);
			for (int i = 0; i < parameterMappings.size(); i++) {
				ParameterMapping parameterMapping = parameterMappings.get(i);
				if (parameterMapping.getMode() != ParameterMode.OUT) {
					Object value;
					String propertyName = parameterMapping.getProperty();
					PropertyTokenizer prop = new PropertyTokenizer(propertyName);
					if (parameterObject == null) {
						value = null;
					} else if (typeHandlerRegistry
							.hasTypeHandler(parameterObject.getClass())) {
						value = parameterObject;
					} else if (boundSql.hasAdditionalParameter(propertyName)) {
						value = boundSql.getAdditionalParameter(propertyName);
					} else if (propertyName
							.startsWith(ForEachSqlNode.ITEM_PREFIX)
							&& boundSql.hasAdditionalParameter(prop.getName())) {
						value = boundSql.getAdditionalParameter(prop.getName());
						if (value != null) {
							value = configuration.newMetaObject(value)
									.getValue(
											propertyName.substring(prop
													.getName().length()));
						}
					} else {
						value = metaObject == null ? null : metaObject
								.getValue(propertyName);
					}
					@SuppressWarnings("rawtypes")
					TypeHandler typeHandler = parameterMapping.getTypeHandler();
					if (typeHandler == null) {
						throw new ExecutorException(
								"There was no TypeHandler found for parameter "
										+ propertyName + " of statement "
										+ mappedStatement.getId());
					}
					typeHandler.setParameter(ps, i + 1, value,
							parameterMapping.getJdbcType());
				}
			}
		}
	}

}
